/**
 * $RCSfile$
 * $Revision: 2407 $
 * $Date: 2004-11-02 15:37:00 -0800 (Tue, 02 Nov 2004) $
 *
 * Copyright 2003-2007 Jive Software.
 *
 * All rights reserved. Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.jivesoftware.smack;

import java.util.Collection;
import java.util.Collections;
import java.util.Map;
import java.util.Set;
import java.util.WeakHashMap;
import java.util.concurrent.CopyOnWriteArraySet;

import org.jivesoftware.smack.filter.AndFilter;
import org.jivesoftware.smack.filter.FromContainsFilter;
import org.jivesoftware.smack.filter.PacketFilter;
import org.jivesoftware.smack.filter.ThreadFilter;
import org.jivesoftware.smack.packet.Message;
import org.jivesoftware.smack.packet.Packet;
import org.jivesoftware.smack.util.StringUtils;
import org.jivesoftware.smack.util.collections.ReferenceMap;

import java.util.*;
import java.util.concurrent.CopyOnWriteArraySet;

/**
 * The chat manager keeps track of references to all current chats. It will not
 * hold any references in memory on its own so it is neccesary to keep a
 * reference to the chat object itself. To be made aware of new chats, register
 * a listener by calling {@link #addChatListener(ChatManagerListener)}.
 * 
 * @author Alexander Wenckus
 */
public class ChatManager {

	/**
	 * Returns the next unique id. Each id made up of a short alphanumeric
	 * prefix along with a unique numeric value.
	 * 
	 * @return the next id.
	 */
	private static synchronized String nextID() {
		return prefix + Long.toString(id++);
	}

	/**
	 * A prefix helps to make sure that ID's are unique across mutliple
	 * instances.
	 */
	private static String prefix = StringUtils.randomString(5);

	/**
	 * Keeps track of the current increment, which is appended to the prefix to
	 * forum a unique ID.
	 */
	private static long id = 0;

	/**
	 * Maps thread ID to chat.
	 */
	private Map<String, Chat> threadChats = Collections
			.synchronizedMap(new ReferenceMap<String, Chat>(ReferenceMap.HARD,
					ReferenceMap.WEAK));

	/**
	 * Maps jids to chats
	 */
	private Map<String, Chat> jidChats = Collections
			.synchronizedMap(new ReferenceMap<String, Chat>(ReferenceMap.HARD,
					ReferenceMap.WEAK));

	/**
	 * Maps base jids to chats
	 */
	private Map<String, Chat> baseJidChats = Collections
			.synchronizedMap(new ReferenceMap<String, Chat>(ReferenceMap.HARD,
					ReferenceMap.WEAK));

	private Set<ChatManagerListener> chatManagerListeners = new CopyOnWriteArraySet<ChatManagerListener>();

	private Map<PacketInterceptor, PacketFilter> interceptors = new WeakHashMap<PacketInterceptor, PacketFilter>();

	private Connection connection;

	ChatManager(Connection connection) {
		this.connection = connection;

		PacketFilter filter = new PacketFilter() {
			public boolean accept(Packet packet) {
				if (!(packet instanceof Message)) {
					return false;
				}
				Message.Type messageType = ((Message) packet).getType();
				return messageType != Message.Type.groupchat
						&& messageType != Message.Type.headline;
			}
		};
		// Add a listener for all message packets so that we can deliver errant
		// messages to the best Chat instance available.
		connection.addPacketListener(new PacketListener() {
			public void processPacket(Packet packet) {
				Message message = (Message) packet;
				Chat chat;
				if (message.getThread() == null) {
					chat = getUserChat(message.getFrom());
				} else {
					chat = getThreadChat(message.getThread());
					if (chat == null) {
						// Try to locate the chat based on the sender of the
						// message
						chat = getUserChat(message.getFrom());
					}
				}

				if (chat == null) {
					chat = createChat(message);
				}
				deliverMessage(chat, message);
			}
		}, filter);
	}

	/**
	 * Creates a new chat and returns it.
	 * 
	 * @param userJID
	 *            the user this chat is with.
	 * @param listener
	 *            the listener which will listen for new messages from this
	 *            chat.
	 * @return the created chat.
	 */
	public Chat createChat(String userJID, MessageListener listener) {
		String threadID;
		do {
			threadID = nextID();
		} while (threadChats.get(threadID) != null);

		return createChat(userJID, threadID, listener);
	}

	/**
	 * Creates a new chat using the specified thread ID, then returns it.
	 * 
	 * @param userJID
	 *            the jid of the user this chat is with
	 * @param thread
	 *            the thread of the created chat.
	 * @param listener
	 *            the listener to add to the chat
	 * @return the created chat.
	 */
	public Chat createChat(String userJID, String thread,
			MessageListener listener) {
		if (thread == null) {
			thread = nextID();
		}
		Chat chat = threadChats.get(thread);
		if (chat != null) {
			throw new IllegalArgumentException("ThreadID is already used");
		}
		chat = createChat(userJID, thread, true);
		chat.addMessageListener(listener);
		return chat;
	}

	private Chat createChat(String userJID, String threadID,
			boolean createdLocally) {
		Chat chat = new Chat(this, userJID, threadID);
		threadChats.put(threadID, chat);
		jidChats.put(userJID, chat);
		baseJidChats.put(StringUtils.parseBareAddress(userJID), chat);

		for (ChatManagerListener listener : chatManagerListeners) {
			listener.chatCreated(chat, createdLocally);
		}

		return chat;
	}

	public synchronized void removeChat(String userJID, Chat chat) {
		if (userJID == null || chat == null)
			return;
		chat.shutdown();

		threadChats.remove(chat.getThreadID());
		jidChats.remove(userJID);
		baseJidChats.remove(StringUtils.parseBareAddress(userJID));
	}

	private Chat createChat(Message message) {
		String threadID = message.getThread();
		if (threadID == null) {
			threadID = nextID();
		}
		String userJID = message.getFrom();

		return createChat(userJID, threadID, false);
	}

	/**
	 * Try to get a matching chat for the given user JID. Try the full JID map
	 * first, the try to match on the base JID if no match is found.
	 * 
	 * @param userJID
	 * @return
	 */
	private Chat getUserChat(String userJID) {
		Chat match = jidChats.get(userJID);

		if (match == null) {
			match = baseJidChats.get(StringUtils.parseBareAddress(userJID));
		}
		return match;
	}

	public Chat getThreadChat(String thread) {
		return threadChats.get(thread);
	}

	/**
	 * Register a new listener with the ChatManager to recieve events related to
	 * chats.
	 * 
	 * @param listener
	 *            the listener.
	 */
	public void addChatListener(ChatManagerListener listener) {
		chatManagerListeners.add(listener);
	}

	/**
	 * Removes a listener, it will no longer be notified of new events related
	 * to chats.
	 * 
	 * @param listener
	 *            the listener that is being removed
	 */
	public void removeChatListener(ChatManagerListener listener) {
		chatManagerListeners.remove(listener);
	}

	/**
	 * Returns an unmodifiable collection of all chat listeners currently
	 * registered with this manager.
	 * 
	 * @return an unmodifiable collection of all chat listeners currently
	 *         registered with this manager.
	 */
	public Collection<ChatManagerListener> getChatListeners() {
		return Collections.unmodifiableCollection(chatManagerListeners);
	}

	private void deliverMessage(Chat chat, Message message) {
		// Here we will run any interceptors
		chat.deliver(message);
	}

	void sendMessage(Chat chat, Message message) {
		for (Map.Entry<PacketInterceptor, PacketFilter> interceptor : interceptors
				.entrySet()) {
			PacketFilter filter = interceptor.getValue();
			if (filter != null && filter.accept(message)) {
				interceptor.getKey().interceptPacket(message);
			}
		}
		// Ensure that messages being sent have a proper FROM value
		if (message.getFrom() == null) {
			message.setFrom(connection.getUser());
		}
		connection.sendPacket(message);
	}

	PacketCollector createPacketCollector(Chat chat) {
		return connection.createPacketCollector(new AndFilter(new ThreadFilter(
				chat.getThreadID()), new FromContainsFilter(chat
				.getParticipant())));
	}

	/**
	 * Adds an interceptor which intercepts any messages sent through chats.
	 * 
	 * @param packetInterceptor
	 *            the interceptor.
	 */
	public void addOutgoingMessageInterceptor(
			PacketInterceptor packetInterceptor) {
		addOutgoingMessageInterceptor(packetInterceptor, null);
	}

	public void addOutgoingMessageInterceptor(
			PacketInterceptor packetInterceptor, PacketFilter filter) {
		if (packetInterceptor != null) {
			interceptors.put(packetInterceptor, filter);
		}
	}
}
